home *** CD-ROM | disk | FTP | other *** search
/ PC World Komputer 2010 April / PCWorld0410.iso / hity wydania / Ubuntu 9.10 PL / karmelkowy-koliberek-desktop-9.10-i386-PL.iso / casper / filesystem.squashfs / usr / lib / xulrunner-1.9.1.5 / modules / utils.js < prev    next >
Text File  |  2009-11-09  |  66KB  |  1,805 lines

  1. /* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
  2. /* ***** BEGIN LICENSE BLOCK *****
  3.  * Version: MPL 1.1/GPL 2.0/LGPL 2.1
  4.  *
  5.  * The contents of this file are subject to the Mozilla Public License Version
  6.  * 1.1 (the "License"); you may not use this file except in compliance with
  7.  * the License. You may obtain a copy of the License at
  8.  * http://www.mozilla.org/MPL/
  9.  *
  10.  * Software distributed under the License is distributed on an "AS IS" basis,
  11.  * WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License
  12.  * for the specific language governing rights and limitations under the
  13.  * License.
  14.  *
  15.  * The Original Code is the Places Command Controller.
  16.  *
  17.  * The Initial Developer of the Original Code is Google Inc.
  18.  * Portions created by the Initial Developer are Copyright (C) 2005
  19.  * the Initial Developer. All Rights Reserved.
  20.  *
  21.  * Contributor(s):
  22.  *   Ben Goodger <beng@google.com>
  23.  *   Myk Melez <myk@mozilla.org>
  24.  *   Asaf Romano <mano@mozilla.com>
  25.  *   Sungjoon Steve Won <stevewon@gmail.com>
  26.  *   Dietrich Ayala <dietrich@mozilla.com>
  27.  *
  28.  * Alternatively, the contents of this file may be used under the terms of
  29.  * either the GNU General Public License Version 2 or later (the "GPL"), or
  30.  * the GNU Lesser General Public License Version 2.1 or later (the "LGPL"),
  31.  * in which case the provisions of the GPL or the LGPL are applicable instead
  32.  * of those above. If you wish to allow use of your version of this file only
  33.  * under the terms of either the GPL or the LGPL, and not to allow others to
  34.  * use your version of this file under the terms of the MPL, indicate your
  35.  * decision by deleting the provisions above and replace them with the notice
  36.  * and other provisions required by the GPL or the LGPL. If you do not delete
  37.  * the provisions above, a recipient may use your version of this file under
  38.  * the terms of any one of the MPL, the GPL or the LGPL.
  39.  *
  40.  * ***** END LICENSE BLOCK ***** */
  41.  
  42. function LOG(str) {
  43.   dump("*** " + str + "\n");
  44. }
  45.  
  46. var EXPORTED_SYMBOLS = ["PlacesUtils"];
  47.  
  48. var Ci = Components.interfaces;
  49. var Cc = Components.classes;
  50. var Cr = Components.results;
  51.  
  52. const EXCLUDE_FROM_BACKUP_ANNO = "places/excludeFromBackup";
  53. const POST_DATA_ANNO = "bookmarkProperties/POSTData";
  54. const READ_ONLY_ANNO = "placesInternal/READ_ONLY";
  55. const LMANNO_FEEDURI = "livemark/feedURI";
  56. const LMANNO_SITEURI = "livemark/siteURI";
  57. const LMANNO_EXPIRATION = "livemark/expiration";
  58. const LMANNO_LOADFAILED = "livemark/loadfailed";
  59. const LMANNO_LOADING = "livemark/loading";
  60.  
  61. // The RESTORE_*_NSIOBSERVER_TOPIC constants should match the #defines of the
  62. // same names in browser/components/places/src/nsPlacesImportExportService.cpp
  63. const RESTORE_BEGIN_NSIOBSERVER_TOPIC = "bookmarks-restore-begin";
  64. const RESTORE_SUCCESS_NSIOBSERVER_TOPIC = "bookmarks-restore-success";
  65. const RESTORE_FAILED_NSIOBSERVER_TOPIC = "bookmarks-restore-failed";
  66. const RESTORE_NSIOBSERVER_DATA = "json";
  67.  
  68. //@line 73 "/build/buildd/xulrunner-1.9.1-1.9.1.5+nobinonly/build-tree/mozilla/toolkit/components/places/src/utils.js"
  69. // On other platforms, the transferable system converts "\r\n" to "\n".
  70. const NEWLINE = "\r\n";
  71. //@line 76 "/build/buildd/xulrunner-1.9.1-1.9.1.5+nobinonly/build-tree/mozilla/toolkit/components/places/src/utils.js"
  72.  
  73. function QI_node(aNode, aIID) {
  74.   var result = null;
  75.   try {
  76.     result = aNode.QueryInterface(aIID);
  77.   }
  78.   catch (e) {
  79.   }
  80.   return result;
  81. }
  82. function asVisit(aNode)    { return QI_node(aNode, Ci.nsINavHistoryVisitResultNode);    }
  83. function asFullVisit(aNode){ return QI_node(aNode, Ci.nsINavHistoryFullVisitResultNode);}
  84. function asContainer(aNode){ return QI_node(aNode, Ci.nsINavHistoryContainerResultNode);}
  85. function asQuery(aNode)    { return QI_node(aNode, Ci.nsINavHistoryQueryResultNode);    }
  86.  
  87. var PlacesUtils = {
  88.   // Place entries that are containers, e.g. bookmark folders or queries.
  89.   TYPE_X_MOZ_PLACE_CONTAINER: "text/x-moz-place-container",
  90.   // Place entries that are bookmark separators.
  91.   TYPE_X_MOZ_PLACE_SEPARATOR: "text/x-moz-place-separator",
  92.   // Place entries that are not containers or separators
  93.   TYPE_X_MOZ_PLACE: "text/x-moz-place",
  94.   // Place entries in shortcut url format (url\ntitle)
  95.   TYPE_X_MOZ_URL: "text/x-moz-url",
  96.   // Place entries formatted as HTML anchors
  97.   TYPE_HTML: "text/html",
  98.   // Place entries as raw URL text
  99.   TYPE_UNICODE: "text/unicode",
  100.  
  101.   /**
  102.    * The Bookmarks Service.
  103.    */
  104.   get bookmarks() {
  105.     delete this.bookmarks;
  106.     return this.bookmarks = Cc["@mozilla.org/browser/nav-bookmarks-service;1"].
  107.                             getService(Ci.nsINavBookmarksService);
  108.   },
  109.  
  110.   /**
  111.    * The Nav History Service.
  112.    */
  113.   get history() {
  114.     delete this.history;
  115.     return this.history = Cc["@mozilla.org/browser/nav-history-service;1"].
  116.                           getService(Ci.nsINavHistoryService);
  117.   },
  118.  
  119.   /**
  120.    * The Live Bookmark Service.
  121.    */
  122.   get livemarks() {
  123.     delete this.livemarks;
  124.     return this.livemarks = Cc["@mozilla.org/browser/livemark-service;2"].
  125.                             getService(Ci.nsILivemarkService);
  126.   },
  127.  
  128.   /**
  129.    * The Annotations Service.
  130.    */
  131.   get annotations() {
  132.     delete this.annotations;
  133.     return this.annotations = Cc["@mozilla.org/browser/annotation-service;1"].
  134.                               getService(Ci.nsIAnnotationService);
  135.   },
  136.  
  137.   /**
  138.    * The Favicons Service
  139.    */
  140.   get favicons() {
  141.     delete this.favicons;
  142.     return this.favicons = Cc["@mozilla.org/browser/favicon-service;1"].
  143.                            getService(Ci.nsIFaviconService);
  144.   },
  145.  
  146.   /**
  147.    * The Places Tagging Service
  148.    */
  149.   get tagging() {
  150.     delete this.tagging;
  151.     return this.tagging = Cc["@mozilla.org/browser/tagging-service;1"].
  152.                           getService(Ci.nsITaggingService);
  153.   },
  154.  
  155.   /**
  156.    * Makes a URI from a spec.
  157.    * @param   aSpec
  158.    *          The string spec of the URI
  159.    * @returns A URI object for the spec.
  160.    */
  161.   _uri: function PU__uri(aSpec) {
  162.     return Cc["@mozilla.org/network/io-service;1"].
  163.            getService(Ci.nsIIOService).
  164.            newURI(aSpec, null, null);
  165.   },
  166.  
  167.   /**
  168.    * String bundle helpers
  169.    */
  170.   get _bundle() {
  171.     const PLACES_STRING_BUNDLE_URI =
  172.         "chrome://places/locale/places.properties";
  173.     delete this._bundle;
  174.     return this._bundle = Cc["@mozilla.org/intl/stringbundle;1"].
  175.                           getService(Ci.nsIStringBundleService).
  176.                           createBundle(PLACES_STRING_BUNDLE_URI);
  177.   },
  178.  
  179.   getFormattedString: function PU_getFormattedString(key, params) {
  180.     return this._bundle.formatStringFromName(key, params, params.length);
  181.   },
  182.  
  183.   getString: function PU_getString(key) {
  184.     return this._bundle.GetStringFromName(key);
  185.   },
  186.  
  187.   /**
  188.    * Determines whether or not a ResultNode is a Bookmark folder.
  189.    * @param   aNode
  190.    *          A result node
  191.    * @returns true if the node is a Bookmark folder, false otherwise
  192.    */
  193.   nodeIsFolder: function PU_nodeIsFolder(aNode) {
  194.     return (aNode.type == Ci.nsINavHistoryResultNode.RESULT_TYPE_FOLDER ||
  195.             aNode.type == Ci.nsINavHistoryResultNode.RESULT_TYPE_FOLDER_SHORTCUT);
  196.   },
  197.  
  198.   /**
  199.    * Determines whether or not a ResultNode represents a bookmarked URI.
  200.    * @param   aNode
  201.    *          A result node
  202.    * @returns true if the node represents a bookmarked URI, false otherwise
  203.    */
  204.   nodeIsBookmark: function PU_nodeIsBookmark(aNode) {
  205.     return aNode.type == Ci.nsINavHistoryResultNode.RESULT_TYPE_URI &&
  206.            aNode.itemId != -1;
  207.   },
  208.  
  209.   /**
  210.    * Determines whether or not a ResultNode is a Bookmark separator.
  211.    * @param   aNode
  212.    *          A result node
  213.    * @returns true if the node is a Bookmark separator, false otherwise
  214.    */
  215.   nodeIsSeparator: function PU_nodeIsSeparator(aNode) {
  216.  
  217.     return (aNode.type == Ci.nsINavHistoryResultNode.RESULT_TYPE_SEPARATOR);
  218.   },
  219.  
  220.   /**
  221.    * Determines whether or not a ResultNode is a visit item.
  222.    * @param   aNode
  223.    *          A result node
  224.    * @returns true if the node is a visit item, false otherwise
  225.    */
  226.   nodeIsVisit: function PU_nodeIsVisit(aNode) {
  227.     var type = aNode.type;
  228.     return type == Ci.nsINavHistoryResultNode.RESULT_TYPE_VISIT ||
  229.            type == Ci.nsINavHistoryResultNode.RESULT_TYPE_FULL_VISIT;
  230.   },
  231.  
  232.   /**
  233.    * Determines whether or not a ResultNode is a URL item.
  234.    * @param   aNode
  235.    *          A result node
  236.    * @returns true if the node is a URL item, false otherwise
  237.    */
  238.   uriTypes: [Ci.nsINavHistoryResultNode.RESULT_TYPE_URI,
  239.              Ci.nsINavHistoryResultNode.RESULT_TYPE_VISIT,
  240.              Ci.nsINavHistoryResultNode.RESULT_TYPE_FULL_VISIT],
  241.   nodeIsURI: function PU_nodeIsURI(aNode) {
  242.     return this.uriTypes.indexOf(aNode.type) != -1;
  243.   },
  244.  
  245.   /**
  246.    * Determines whether or not a ResultNode is a Query item.
  247.    * @param   aNode
  248.    *          A result node
  249.    * @returns true if the node is a Query item, false otherwise
  250.    */
  251.   nodeIsQuery: function PU_nodeIsQuery(aNode) {
  252.     return aNode.type == Ci.nsINavHistoryResultNode.RESULT_TYPE_QUERY;
  253.   },
  254.  
  255.   /**
  256.    * Determines if a node is read only (children cannot be inserted, sometimes
  257.    * they cannot be removed depending on the circumstance)
  258.    * @param   aNode
  259.    *          A result node
  260.    * @returns true if the node is readonly, false otherwise
  261.    */
  262.   nodeIsReadOnly: function PU_nodeIsReadOnly(aNode) {
  263.     if (this.nodeIsFolder(aNode) || this.nodeIsDynamicContainer(aNode))
  264.       return this.bookmarks.getFolderReadonly(this.getConcreteItemId(aNode));
  265.     if (this.nodeIsQuery(aNode) &&
  266.         asQuery(aNode).queryOptions.resultType !=
  267.           Ci.nsINavHistoryQueryOptions.RESULTS_AS_TAG_CONTENTS)
  268.       return aNode.childrenReadOnly;
  269.     return false;
  270.   },
  271.  
  272.   /**
  273.    * Determines whether or not a ResultNode is a host container.
  274.    * @param   aNode
  275.    *          A result node
  276.    * @returns true if the node is a host container, false otherwise
  277.    */
  278.   nodeIsHost: function PU_nodeIsHost(aNode) {
  279.     return aNode.type == Ci.nsINavHistoryResultNode.RESULT_TYPE_QUERY &&
  280.            aNode.parent &&
  281.            asQuery(aNode.parent).queryOptions.resultType ==
  282.              Ci.nsINavHistoryQueryOptions.RESULTS_AS_SITE_QUERY;
  283.   },
  284.  
  285.   /**
  286.    * Determines whether or not a ResultNode is a day container.
  287.    * @param   node
  288.    *          A NavHistoryResultNode
  289.    * @returns true if the node is a day container, false otherwise
  290.    */
  291.   nodeIsDay: function PU_nodeIsDay(aNode) {
  292.     var resultType;
  293.     return aNode.type == Ci.nsINavHistoryResultNode.RESULT_TYPE_QUERY &&
  294.            aNode.parent &&
  295.            ((resultType = asQuery(aNode.parent).queryOptions.resultType) ==
  296.                Ci.nsINavHistoryQueryOptions.RESULTS_AS_DATE_QUERY ||
  297.              resultType == Ci.nsINavHistoryQueryOptions.RESULTS_AS_DATE_SITE_QUERY);
  298.   },
  299.  
  300.   /**
  301.    * Determines whether or not a result-node is a tag container.
  302.    * @param   aNode
  303.    *          A result-node
  304.    * @returns true if the node is a tag container, false otherwise
  305.    */
  306.   nodeIsTagQuery: function PU_nodeIsTagQuery(aNode) {
  307.     return aNode.type == Ci.nsINavHistoryResultNode.RESULT_TYPE_QUERY &&
  308.            asQuery(aNode).queryOptions.resultType ==
  309.              Ci.nsINavHistoryQueryOptions.RESULTS_AS_TAG_CONTENTS;
  310.   },
  311.  
  312.   /**
  313.    * Determines whether or not a ResultNode is a container.
  314.    * @param   aNode
  315.    *          A result node
  316.    * @returns true if the node is a container item, false otherwise
  317.    */
  318.   containerTypes: [Ci.nsINavHistoryResultNode.RESULT_TYPE_FOLDER,
  319.                    Ci.nsINavHistoryResultNode.RESULT_TYPE_FOLDER_SHORTCUT,
  320.                    Ci.nsINavHistoryResultNode.RESULT_TYPE_QUERY,
  321.                    Ci.nsINavHistoryResultNode.RESULT_TYPE_DYNAMIC_CONTAINER],
  322.   nodeIsContainer: function PU_nodeIsContainer(aNode) {
  323.     return this.containerTypes.indexOf(aNode.type) != -1;
  324.   },
  325.  
  326.   /**
  327.    * Determines whether or not a ResultNode is an history related container.
  328.    * @param   node
  329.    *          A result node
  330.    * @returns true if the node is an history related container, false otherwise
  331.    */
  332.   nodeIsHistoryContainer: function PU_nodeIsHistoryContainer(aNode) {
  333.     var resultType;
  334.     return this.nodeIsQuery(aNode) &&
  335.            ((resultType = asQuery(aNode).queryOptions.resultType) ==
  336.               Ci.nsINavHistoryQueryOptions.RESULTS_AS_DATE_SITE_QUERY ||
  337.             resultType == Ci.nsINavHistoryQueryOptions.RESULTS_AS_DATE_QUERY ||
  338.             resultType == Ci.nsINavHistoryQueryOptions.RESULTS_AS_SITE_QUERY ||
  339.             this.nodeIsDay(aNode) ||
  340.             this.nodeIsHost(aNode));
  341.   },
  342.  
  343.   /**
  344.    * Determines whether or not a result-node is a dynamic-container item.
  345.    * The dynamic container result node type is for dynamically created
  346.    * containers (e.g. for the file browser service where you get your folders
  347.    * in bookmark menus).
  348.    * @param   aNode
  349.    *          A result node
  350.    * @returns true if the node is a dynamic container item, false otherwise
  351.    */
  352.   nodeIsDynamicContainer: function PU_nodeIsDynamicContainer(aNode) {
  353.     if (aNode.type == Ci.nsINavHistoryResultNode.RESULT_TYPE_DYNAMIC_CONTAINER)
  354.       return true;
  355.     return false;
  356.   },
  357.  
  358.  /**
  359.   * Determines whether a result node is a remote container registered by the
  360.   * livemark service.
  361.   * @param aNode
  362.   *        A result Node
  363.   * @returns true if the node is a livemark container item
  364.   */
  365.   nodeIsLivemarkContainer: function PU_nodeIsLivemarkContainer(aNode) {
  366.     // Use the annotations service directly to avoid instantiating
  367.     // the Livemark service on startup. (bug 398300)
  368.     return this.nodeIsFolder(aNode) &&
  369.            this.annotations.itemHasAnnotation(aNode.itemId, LMANNO_FEEDURI);
  370.   },
  371.  
  372.  /**
  373.   * Determines whether a result node is a live-bookmark item
  374.   * @param aNode
  375.   *        A result node
  376.   * @returns true if the node is a livemark container item
  377.   */
  378.   nodeIsLivemarkItem: function PU_nodeIsLivemarkItem(aNode) {
  379.     return aNode.parent && this.nodeIsLivemarkContainer(aNode.parent);
  380.   },
  381.  
  382.   /**
  383.    * Determines whether or not a node is a readonly folder.
  384.    * @param   aNode
  385.    *          The node to test.
  386.    * @returns true if the node is a readonly folder.
  387.   */
  388.   isReadonlyFolder: function(aNode) {
  389.     return this.nodeIsFolder(aNode) &&
  390.            this.bookmarks.getFolderReadonly(asQuery(aNode).folderItemId);
  391.   },
  392.  
  393.   /**
  394.    * Gets the concrete item-id for the given node. Generally, this is just
  395.    * node.itemId, but for folder-shortcuts that's node.folderItemId.
  396.    */
  397.   getConcreteItemId: function PU_getConcreteItemId(aNode) {
  398.     if (aNode.type == Ci.nsINavHistoryResultNode.RESULT_TYPE_FOLDER_SHORTCUT)
  399.       return asQuery(aNode).folderItemId;
  400.     else if (PlacesUtils.nodeIsTagQuery(aNode)) {
  401.       // RESULTS_AS_TAG_CONTENTS queries are similar to folder shortcuts
  402.       // so we can still get the concrete itemId for them.
  403.       var queries = aNode.getQueries({});
  404.       var folders = queries[0].getFolders({});
  405.       return folders[0];
  406.     }
  407.     return aNode.itemId;
  408.   },
  409.  
  410.   /**
  411.    * Gets the index of a node within its parent container
  412.    * @param   aNode
  413.    *          The node to look up
  414.    * @returns The index of the node within its parent container, or -1 if the
  415.    *          node was not found or the node specified has no parent.
  416.    */
  417.   getIndexOfNode: function PU_getIndexOfNode(aNode) {
  418.     var parent = aNode.parent;
  419.     if (!parent)
  420.       return -1;
  421.     var wasOpen = parent.containerOpen;
  422.     var result, oldViewer;
  423.     if (!wasOpen) {
  424.       result = parent.parentResult;
  425.       oldViewer = result.viewer;
  426.       result.viewer = null;
  427.       parent.containerOpen = true;
  428.     }
  429.     var cc = parent.childCount;
  430.     for (var i = 0; i < cc && parent.getChild(i) != aNode; ++i);
  431.     if (!wasOpen) {
  432.       parent.containerOpen = false;
  433.       result.viewer = oldViewer;
  434.     }
  435.     return i < cc ? i : -1;
  436.   },
  437.  
  438.   /**
  439.    * String-wraps a result node according to the rules of the specified
  440.    * content type.
  441.    * @param   aNode
  442.    *          The Result node to wrap (serialize)
  443.    * @param   aType
  444.    *          The content type to serialize as
  445.    * @param   [optional] aOverrideURI
  446.    *          Used instead of the node's URI if provided.
  447.    *          This is useful for wrapping a container as TYPE_X_MOZ_URL,
  448.    *          TYPE_HTML or TYPE_UNICODE.
  449.    * @param   aForceCopy
  450.    *          Does a full copy, resolving folder shortcuts.
  451.    * @returns A string serialization of the node
  452.    */
  453.   wrapNode: function PU_wrapNode(aNode, aType, aOverrideURI, aForceCopy) {
  454.     var self = this;
  455.  
  456.     // when wrapping a node, we want all the items, even if the original
  457.     // query options are excluding them.
  458.     // this can happen when copying from the left hand pane of the bookmarks
  459.     // organizer
  460.     function convertNode(cNode) {
  461.       if (self.nodeIsFolder(cNode) && asQuery(cNode).queryOptions.excludeItems) {
  462.         var concreteId = self.getConcreteItemId(cNode);
  463.         return self.getFolderContents(concreteId, false, true).root;
  464.       }
  465.       return cNode;
  466.     }
  467.  
  468.     switch (aType) {
  469.       case this.TYPE_X_MOZ_PLACE:
  470.       case this.TYPE_X_MOZ_PLACE_SEPARATOR:
  471.       case this.TYPE_X_MOZ_PLACE_CONTAINER:
  472.         var writer = {
  473.           value: "",
  474.           write: function PU_wrapNode__write(aStr, aLen) {
  475.             this.value += aStr;
  476.           }
  477.         };
  478.         self.serializeNodeAsJSONToOutputStream(convertNode(aNode), writer, true, aForceCopy);
  479.         return writer.value;
  480.       case this.TYPE_X_MOZ_URL:
  481.         function gatherDataUrl(bNode) {
  482.           if (self.nodeIsLivemarkContainer(bNode)) {
  483.             var siteURI = self.livemarks.getSiteURI(bNode.itemId).spec;
  484.             return siteURI + NEWLINE + bNode.title;
  485.           }
  486.           if (self.nodeIsURI(bNode))
  487.             return (aOverrideURI || bNode.uri) + NEWLINE + bNode.title;
  488.           // ignore containers and separators - items without valid URIs
  489.           return "";
  490.         }
  491.         return gatherDataUrl(convertNode(aNode));
  492.  
  493.       case this.TYPE_HTML:
  494.         function gatherDataHtml(bNode) {
  495.           function htmlEscape(s) {
  496.             s = s.replace(/&/g, "&");
  497.             s = s.replace(/>/g, ">");
  498.             s = s.replace(/</g, "<");
  499.             s = s.replace(/"/g, """);
  500.             s = s.replace(/'/g, "'");
  501.             return s;
  502.           }
  503.           // escape out potential HTML in the title
  504.           var escapedTitle = bNode.title ? htmlEscape(bNode.title) : "";
  505.           if (self.nodeIsLivemarkContainer(bNode)) {
  506.             var siteURI = self.livemarks.getSiteURI(bNode.itemId).spec;
  507.             return "<A HREF=\"" + siteURI + "\">" + escapedTitle + "</A>" + NEWLINE;
  508.           }
  509.           if (self.nodeIsContainer(bNode)) {
  510.             asContainer(bNode);
  511.             var wasOpen = bNode.containerOpen;
  512.             if (!wasOpen)
  513.               bNode.containerOpen = true;
  514.  
  515.             var childString = "<DL><DT>" + escapedTitle + "</DT>" + NEWLINE;
  516.             var cc = bNode.childCount;
  517.             for (var i = 0; i < cc; ++i)
  518.               childString += "<DD>"
  519.                              + NEWLINE
  520.                              + gatherDataHtml(bNode.getChild(i))
  521.                              + "</DD>"
  522.                              + NEWLINE;
  523.             bNode.containerOpen = wasOpen;
  524.             return childString + "</DL>" + NEWLINE;
  525.           }
  526.           if (self.nodeIsURI(bNode))
  527.             return "<A HREF=\"" + bNode.uri + "\">" + escapedTitle + "</A>" + NEWLINE;
  528.           if (self.nodeIsSeparator(bNode))
  529.             return "<HR>" + NEWLINE;
  530.           return "";
  531.         }
  532.         return gatherDataHtml(convertNode(aNode));
  533.     }
  534.     // case this.TYPE_UNICODE:
  535.     function gatherDataText(bNode) {
  536.       if (self.nodeIsLivemarkContainer(bNode))
  537.         return self.livemarks.getSiteURI(bNode.itemId).spec;
  538.       if (self.nodeIsContainer(bNode)) {
  539.         asContainer(bNode);
  540.         var wasOpen = bNode.containerOpen;
  541.         if (!wasOpen)
  542.           bNode.containerOpen = true;
  543.  
  544.         var childString = bNode.title + NEWLINE;
  545.         var cc = bNode.childCount;
  546.         for (var i = 0; i < cc; ++i) {
  547.           var child = bNode.getChild(i);
  548.           var suffix = i < (cc - 1) ? NEWLINE : "";
  549.           childString += gatherDataText(child) + suffix;
  550.         }
  551.         bNode.containerOpen = wasOpen;
  552.         return childString;
  553.       }
  554.       if (self.nodeIsURI(bNode))
  555.         return (aOverrideURI || bNode.uri);
  556.       if (self.nodeIsSeparator(bNode))
  557.         return "--------------------";
  558.       return "";
  559.     }
  560.  
  561.     return gatherDataText(convertNode(aNode));
  562.   },
  563.  
  564.   /**
  565.    * Unwraps data from the Clipboard or the current Drag Session.
  566.    * @param   blob
  567.    *          A blob (string) of data, in some format we potentially know how
  568.    *          to parse.
  569.    * @param   type
  570.    *          The content type of the blob.
  571.    * @returns An array of objects representing each item contained by the source.
  572.    */
  573.   unwrapNodes: function PU_unwrapNodes(blob, type) {
  574.     // We split on "\n"  because the transferable system converts "\r\n" to "\n"
  575.     var nodes = [];
  576.     switch(type) {
  577.       case this.TYPE_X_MOZ_PLACE:
  578.       case this.TYPE_X_MOZ_PLACE_SEPARATOR:
  579.       case this.TYPE_X_MOZ_PLACE_CONTAINER:
  580.         var JSON = Cc["@mozilla.org/dom/json;1"].createInstance(Ci.nsIJSON);
  581.         nodes = JSON.decode("[" + blob + "]");
  582.         break;
  583.       case this.TYPE_X_MOZ_URL:
  584.         var parts = blob.split("\n");
  585.         // data in this type has 2 parts per entry, so if there are fewer
  586.         // than 2 parts left, the blob is malformed and we should stop
  587.         // but drag and drop of files from the shell has parts.length = 1
  588.         if (parts.length != 1 && parts.length % 2)
  589.           break;
  590.         for (var i = 0; i < parts.length; i=i+2) {
  591.           var uriString = parts[i];
  592.           var titleString = "";
  593.           if (parts.length > i+1)
  594.             titleString = parts[i+1];
  595.           else {
  596.             // for drag and drop of files, try to use the leafName as title
  597.             try {
  598.               titleString = this._uri(uriString).QueryInterface(Ci.nsIURL)
  599.                               .fileName;
  600.             }
  601.             catch (e) {}
  602.           }
  603.           // note:  this._uri() will throw if uriString is not a valid URI
  604.           if (this._uri(uriString)) {
  605.             nodes.push({ uri: uriString,
  606.                          title: titleString ? titleString : uriString ,
  607.                          type: this.TYPE_X_MOZ_URL });
  608.           }
  609.         }
  610.         break;
  611.       case this.TYPE_UNICODE:
  612.         var parts = blob.split("\n");
  613.         for (var i = 0; i < parts.length; i++) {
  614.           var uriString = parts[i];
  615.           // text/uri-list is converted to TYPE_UNICODE but it could contain
  616.           // comments line prepended by #, we should skip them
  617.           if (uriString.substr(0, 1) == '\x23')
  618.             continue;
  619.           // note: this._uri() will throw if uriString is not a valid URI
  620.           if (uriString != "" && this._uri(uriString))
  621.             nodes.push({ uri: uriString,
  622.                          title: uriString,
  623.                          type: this.TYPE_X_MOZ_URL });
  624.         }
  625.         break;
  626.       default:
  627.         LOG("Cannot unwrap data of type " + type);
  628.         throw Cr.NS_ERROR_INVALID_ARG;
  629.     }
  630.     return nodes;
  631.   },
  632.  
  633.   /**
  634.    * Generates a nsINavHistoryResult for the contents of a folder.
  635.    * @param   folderId
  636.    *          The folder to open
  637.    * @param   [optional] excludeItems
  638.    *          True to hide all items (individual bookmarks). This is used on
  639.    *          the left places pane so you just get a folder hierarchy.
  640.    * @param   [optional] expandQueries
  641.    *          True to make query items expand as new containers. For managing,
  642.    *          you want this to be false, for menus and such, you want this to
  643.    *          be true.
  644.    * @returns A nsINavHistoryResult containing the contents of the
  645.    *          folder. The result.root is guaranteed to be open.
  646.    */
  647.   getFolderContents:
  648.   function PU_getFolderContents(aFolderId, aExcludeItems, aExpandQueries) {
  649.     var query = this.history.getNewQuery();
  650.     query.setFolders([aFolderId], 1);
  651.     var options = this.history.getNewQueryOptions();
  652.     options.excludeItems = aExcludeItems;
  653.     options.expandQueries = aExpandQueries;
  654.  
  655.     var result = this.history.executeQuery(query, options);
  656.     result.root.containerOpen = true;
  657.     return result;
  658.   },
  659.  
  660.   /**
  661.    * Fetch all annotations for a URI, including all properties of each
  662.    * annotation which would be required to recreate it.
  663.    * @param aURI
  664.    *        The URI for which annotations are to be retrieved.
  665.    * @return Array of objects, each containing the following properties:
  666.    *         name, flags, expires, mimeType, type, value
  667.    */
  668.   getAnnotationsForURI: function PU_getAnnotationsForURI(aURI) {
  669.     var annosvc = this.annotations;
  670.     var annos = [], val = null;
  671.     var annoNames = annosvc.getPageAnnotationNames(aURI, {});
  672.     for (var i = 0; i < annoNames.length; i++) {
  673.       var flags = {}, exp = {}, mimeType = {}, storageType = {};
  674.       annosvc.getPageAnnotationInfo(aURI, annoNames[i], flags, exp, mimeType, storageType);
  675.       if (storageType.value == annosvc.TYPE_BINARY) {
  676.         var data = {}, length = {}, mimeType = {};
  677.         annosvc.getPageAnnotationBinary(aURI, annoNames[i], data, length, mimeType);
  678.         val = data.value;
  679.       }
  680.       else
  681.         val = annosvc.getPageAnnotation(aURI, annoNames[i]);
  682.  
  683.       annos.push({name: annoNames[i],
  684.                   flags: flags.value,
  685.                   expires: exp.value,
  686.                   mimeType: mimeType.value,
  687.                   type: storageType.value,
  688.                   value: val});
  689.     }
  690.     return annos;
  691.   },
  692.  
  693.   /**
  694.    * Fetch all annotations for an item, including all properties of each
  695.    * annotation which would be required to recreate it.
  696.    * @param aItemId
  697.    *        The identifier of the itme for which annotations are to be
  698.    *        retrieved.
  699.    * @return Array of objects, each containing the following properties:
  700.    *         name, flags, expires, mimeType, type, value
  701.    */
  702.   getAnnotationsForItem: function PU_getAnnotationsForItem(aItemId) {
  703.     var annosvc = this.annotations;
  704.     var annos = [], val = null;
  705.     var annoNames = annosvc.getItemAnnotationNames(aItemId, {});
  706.     for (var i = 0; i < annoNames.length; i++) {
  707.       var flags = {}, exp = {}, mimeType = {}, storageType = {};
  708.       annosvc.getItemAnnotationInfo(aItemId, annoNames[i], flags, exp, mimeType, storageType);
  709.       if (storageType.value == annosvc.TYPE_BINARY) {
  710.         var data = {}, length = {}, mimeType = {};
  711.         annosvc.geItemAnnotationBinary(aItemId, annoNames[i], data, length, mimeType);
  712.         val = data.value;
  713.       }
  714.       else
  715.         val = annosvc.getItemAnnotation(aItemId, annoNames[i]);
  716.  
  717.       annos.push({name: annoNames[i],
  718.                   flags: flags.value,
  719.                   expires: exp.value,
  720.                   mimeType: mimeType.value,
  721.                   type: storageType.value,
  722.                   value: val});
  723.     }
  724.     return annos;
  725.   },
  726.  
  727.   /**
  728.    * Annotate a URI with a batch of annotations.
  729.    * @param aURI
  730.    *        The URI for which annotations are to be set.
  731.    * @param aAnnotations
  732.    *        Array of objects, each containing the following properties:
  733.    *        name, flags, expires, type, mimeType (only used for binary
  734.    *        annotations) value.
  735.    *        If the value for an annotation is not set it will be removed.
  736.    */
  737.   setAnnotationsForURI: function PU_setAnnotationsForURI(aURI, aAnnos) {
  738.     var annosvc = this.annotations;
  739.     aAnnos.forEach(function(anno) {
  740.       if (!anno.value) {
  741.         annosvc.removePageAnnotation(aURI, anno.name);
  742.         return;
  743.       }
  744.       var flags = ("flags" in anno) ? anno.flags : 0;
  745.       var expires = ("expires" in anno) ?
  746.         anno.expires : Ci.nsIAnnotationService.EXPIRE_NEVER;
  747.       if (anno.type == annosvc.TYPE_BINARY) {
  748.         annosvc.setPageAnnotationBinary(aURI, anno.name, anno.value,
  749.                                         anno.value.length, anno.mimeType,
  750.                                         flags, expires);
  751.       }
  752.       else
  753.         annosvc.setPageAnnotation(aURI, anno.name, anno.value, flags, expires);
  754.     });
  755.   },
  756.  
  757.   /**
  758.    * Annotate an item with a batch of annotations.
  759.    * @param aItemId
  760.    *        The identifier of the item for which annotations are to be set
  761.    * @param aAnnotations
  762.    *        Array of objects, each containing the following properties:
  763.    *        name, flags, expires, type, mimeType (only used for binary
  764.    *        annotations) value.
  765.    *        If the value for an annotation is not set it will be removed.
  766.    */
  767.   setAnnotationsForItem: function PU_setAnnotationsForItem(aItemId, aAnnos) {
  768.     var annosvc = this.annotations;
  769.  
  770.     aAnnos.forEach(function(anno) {
  771.       if (!anno.value) {
  772.         annosvc.removeItemAnnotation(aItemId, anno.name);
  773.         return;
  774.       }
  775.       var flags = ("flags" in anno) ? anno.flags : 0;
  776.       var expires = ("expires" in anno) ?
  777.         anno.expires : Ci.nsIAnnotationService.EXPIRE_NEVER;
  778.       if (anno.type == annosvc.TYPE_BINARY) {
  779.         annosvc.setItemAnnotationBinary(aItemId, anno.name, anno.value,
  780.                                         anno.value.length, anno.mimeType,
  781.                                         flags, expires);
  782.       }
  783.       else {
  784.         annosvc.setItemAnnotation(aItemId, anno.name, anno.value, flags,
  785.                                   expires);
  786.       }
  787.     });
  788.   },
  789.  
  790.   // identifier getters for special folders
  791.   get placesRootId() {
  792.     delete this.placesRootId;
  793.     return this.placesRootId = this.bookmarks.placesRoot;
  794.   },
  795.  
  796.   get bookmarksMenuFolderId() {
  797.     delete this.bookmarksMenuFolderId;
  798.     return this.bookmarksMenuFolderId = this.bookmarks.bookmarksMenuFolder;
  799.   },
  800.  
  801.   get toolbarFolderId() {
  802.     delete this.toolbarFolderId;
  803.     return this.toolbarFolderId = this.bookmarks.toolbarFolder;
  804.   },
  805.  
  806.   get tagsFolderId() {
  807.     delete this.tagsFolderId;
  808.     return this.tagsFolderId = this.bookmarks.tagsFolder;
  809.   },
  810.  
  811.   get unfiledBookmarksFolderId() {
  812.     delete this.unfiledBookmarksFolderId;
  813.     return this.unfiledBookmarksFolderId = this.bookmarks.unfiledBookmarksFolder;
  814.   },
  815.  
  816.   /**
  817.    * Checks if aItemId is a root.
  818.    *
  819.    *   @param aItemId
  820.    *          item id to look for.
  821.    *   @returns true if aItemId is a root, false otherwise.
  822.    */
  823.   isRootItem: function PU_isRootItem(aItemId) {
  824.     return aItemId == PlacesUtils.bookmarksMenuFolderId ||
  825.            aItemId == PlacesUtils.toolbarFolderId ||
  826.            aItemId == PlacesUtils.unfiledBookmarksFolderId ||
  827.            aItemId == PlacesUtils.tagsFolderId ||
  828.            aItemId == PlacesUtils.placesRootId;
  829.   },
  830.  
  831.   /**
  832.    * Set the POST data associated with a bookmark, if any.
  833.    * Used by POST keywords.
  834.    *   @param aBookmarkId
  835.    *   @returns string of POST data
  836.    */
  837.   setPostDataForBookmark: function PU_setPostDataForBookmark(aBookmarkId, aPostData) {
  838.     const annos = this.annotations;
  839.     if (aPostData)
  840.       annos.setItemAnnotation(aBookmarkId, POST_DATA_ANNO, aPostData, 
  841.                               0, Ci.nsIAnnotationService.EXPIRE_NEVER);
  842.     else if (annos.itemHasAnnotation(aBookmarkId, POST_DATA_ANNO))
  843.       annos.removeItemAnnotation(aBookmarkId, POST_DATA_ANNO);
  844.   },
  845.  
  846.   /**
  847.    * Get the POST data associated with a bookmark, if any.
  848.    * @param aBookmarkId
  849.    * @returns string of POST data if set for aBookmarkId. null otherwise.
  850.    */
  851.   getPostDataForBookmark: function PU_getPostDataForBookmark(aBookmarkId) {
  852.     const annos = this.annotations;
  853.     if (annos.itemHasAnnotation(aBookmarkId, POST_DATA_ANNO))
  854.       return annos.getItemAnnotation(aBookmarkId, POST_DATA_ANNO);
  855.  
  856.     return null;
  857.   },
  858.  
  859.   /**
  860.    * Get the URI (and any associated POST data) for a given keyword.
  861.    * @param aKeyword string keyword
  862.    * @returns an array containing a string URL and a string of POST data
  863.    */
  864.   getURLAndPostDataForKeyword: function PU_getURLAndPostDataForKeyword(aKeyword) {
  865.     var url = null, postdata = null;
  866.     try {
  867.       var uri = this.bookmarks.getURIForKeyword(aKeyword);
  868.       if (uri) {
  869.         url = uri.spec;
  870.         var bookmarks = this.bookmarks.getBookmarkIdsForURI(uri, {});
  871.         for (let i = 0; i < bookmarks.length; i++) {
  872.           var bookmark = bookmarks[i];
  873.           var kw = this.bookmarks.getKeywordForBookmark(bookmark);
  874.           if (kw == aKeyword) {
  875.             postdata = this.getPostDataForBookmark(bookmark);
  876.             break;
  877.           }
  878.         }
  879.       }
  880.     } catch(ex) {}
  881.     return [url, postdata];
  882.   },
  883.  
  884.   /**
  885.    * Get all bookmarks for a URL, excluding items under tag or livemark
  886.    * containers.
  887.    */
  888.   getBookmarksForURI:
  889.   function PU_getBookmarksForURI(aURI) {
  890.     var bmkIds = this.bookmarks.getBookmarkIdsForURI(aURI, {});
  891.  
  892.     // filter the ids list
  893.     return bmkIds.filter(function(aID) {
  894.       var parent = this.bookmarks.getFolderIdForItem(aID);
  895.       // Livemark child
  896.       if (this.annotations.itemHasAnnotation(parent, LMANNO_FEEDURI))
  897.         return false;
  898.       var grandparent = this.bookmarks.getFolderIdForItem(parent);
  899.       // item under a tag container
  900.       if (grandparent == this.tagsFolderId)
  901.         return false;
  902.       return true;
  903.     }, this);
  904.   },
  905.  
  906.   /**
  907.    * Get the most recently added/modified bookmark for a URL, excluding items
  908.    * under tag or livemark containers. -1 is returned if no item is found.
  909.    */
  910.   getMostRecentBookmarkForURI:
  911.   function PU_getMostRecentBookmarkForURI(aURI) {
  912.     var bmkIds = this.bookmarks.getBookmarkIdsForURI(aURI, {});
  913.     for (var i = 0; i < bmkIds.length; i++) {
  914.       // Find the first folder which isn't a tag container
  915.       var bk = bmkIds[i];
  916.       var parent = this.bookmarks.getFolderIdForItem(bk);
  917.       if (parent == this.unfiledBookmarksFolderId)
  918.         return bk;
  919.  
  920.       var grandparent = this.bookmarks.getFolderIdForItem(parent);
  921.       if (grandparent != this.tagsFolderId &&
  922.           !this.annotations.itemHasAnnotation(parent, LMANNO_FEEDURI))
  923.         return bk;
  924.     }
  925.     return -1;
  926.   },
  927.  
  928.   getMostRecentFolderForFeedURI:
  929.   function PU_getMostRecentFolderForFeedURI(aURI) {
  930.     var feedSpec = aURI.spec
  931.     var annosvc = this.annotations;
  932.     var livemarks = annosvc.getItemsWithAnnotation(LMANNO_FEEDURI, {});
  933.     for (var i = 0; i < livemarks.length; i++) {
  934.       if (annosvc.getItemAnnotation(livemarks[i], LMANNO_FEEDURI) == feedSpec)
  935.         return livemarks[i];
  936.     }
  937.     return -1;
  938.   },
  939.  
  940.   /**
  941.    * Returns a nsNavHistoryContainerResultNode with forced excludeItems and
  942.    * expandQueries.
  943.    * @param   aNode
  944.    *          The node to convert
  945.    * @param   [optional] excludeItems
  946.    *          True to hide all items (individual bookmarks). This is used on
  947.    *          the left places pane so you just get a folder hierarchy.
  948.    * @param   [optional] expandQueries
  949.    *          True to make query items expand as new containers. For managing,
  950.    *          you want this to be false, for menus and such, you want this to
  951.    *          be true.
  952.    * @returns A nsINavHistoryContainerResultNode containing the unfiltered
  953.    *          contents of the container.
  954.    * @note    The returned container node could be open or closed, we don't
  955.    *          guarantee its status.
  956.    */
  957.   getContainerNodeWithOptions:
  958.   function PU_getContainerNodeWithOptions(aNode, aExcludeItems, aExpandQueries) {
  959.     if (!this.nodeIsContainer(aNode))
  960.       throw Cr.NS_ERROR_INVALID_ARG;
  961.  
  962.     // excludeItems is inherited by child containers in an excludeItems view.
  963.     var excludeItems = asQuery(aNode).queryOptions.excludeItems ||
  964.                        asQuery(aNode.parentResult.root).queryOptions.excludeItems;
  965.     // expandQueries is inherited by child containers in an expandQueries view.
  966.     var expandQueries = asQuery(aNode).queryOptions.expandQueries &&
  967.                         asQuery(aNode.parentResult.root).queryOptions.expandQueries;
  968.  
  969.     // If our options are exactly what we expect, directly return the node.
  970.     if (excludeItems == aExcludeItems && expandQueries == aExpandQueries)
  971.       return aNode;
  972.  
  973.     // Otherwise, get contents manually.
  974.     var queries = {}, options = {};
  975.     this.history.queryStringToQueries(aNode.uri, queries, {}, options);
  976.     options.value.excludeItems = aExcludeItems;
  977.     options.value.expandQueries = aExpandQueries;
  978.     return this.history.executeQueries(queries.value,
  979.                                        queries.value.length,
  980.                                        options.value).root;
  981.   },
  982.  
  983.   /**
  984.    * Returns true if a container has uri nodes in its first level.
  985.    * Has better performance than (getURLsForContainerNode(node).length > 0).
  986.    * @param aNode
  987.    *        The container node to search through.
  988.    * @returns true if the node contains uri nodes, false otherwise.
  989.    */
  990.   hasChildURIs: function PU_hasChildURIs(aNode) {
  991.     if (!this.nodeIsContainer(aNode))
  992.       return false;
  993.  
  994.     var root = this.getContainerNodeWithOptions(aNode, false, true);
  995.     var oldViewer = root.parentResult.viewer;
  996.     var wasOpen = root.containerOpen;
  997.     if (!wasOpen) {
  998.       root.parentResult.viewer = null;
  999.       root.containerOpen = true;
  1000.     }
  1001.  
  1002.     var found = false;
  1003.     for (var i = 0; i < root.childCount && !found; i++) {
  1004.       var child = root.getChild(i);
  1005.       if (this.nodeIsURI(child))
  1006.         found = true;
  1007.     }
  1008.  
  1009.     if (!wasOpen) {
  1010.       root.containerOpen = false;
  1011.       root.parentResult.viewer = oldViewer;
  1012.     }
  1013.     return found;
  1014.   },
  1015.  
  1016.   /**
  1017.    * Returns an array containing all the uris in the first level of the
  1018.    * passed in container.
  1019.    * If you only need to know if the node contains uris, use hasChildURIs.
  1020.    * @param aNode
  1021.    *        The container node to search through
  1022.    * @returns array of uris in the first level of the container.
  1023.    */
  1024.   getURLsForContainerNode: function PU_getURLsForContainerNode(aNode) {
  1025.     var urls = [];
  1026.     if (!this.nodeIsContainer(aNode))
  1027.       return urls;
  1028.  
  1029.     var root = this.getContainerNodeWithOptions(aNode, false, true);
  1030.     var oldViewer = root.parentResult.viewer;
  1031.     var wasOpen = root.containerOpen;
  1032.     if (!wasOpen) {
  1033.       root.parentResult.viewer = null;
  1034.       root.containerOpen = true;
  1035.     }
  1036.  
  1037.    for (var i = 0; i < root.childCount; ++i) {
  1038.       var child = root.getChild(i);
  1039.       if (this.nodeIsURI(child))
  1040.         urls.push({uri: child.uri, isBookmark: this.nodeIsBookmark(child)});
  1041.     }
  1042.  
  1043.     if (!wasOpen) {
  1044.       root.containerOpen = false;
  1045.       root.parentResult.viewer = oldViewer;
  1046.     }
  1047.     return urls;
  1048.   },
  1049.  
  1050.   /**
  1051.    * Restores bookmarks/tags from a JSON file.
  1052.    * WARNING: This method *removes* any bookmarks in the collection before
  1053.    * restoring from the file.
  1054.    *
  1055.    * @param aFile
  1056.    *        nsIFile of bookmarks in JSON format to be restored.
  1057.    */
  1058.   restoreBookmarksFromJSONFile:
  1059.   function PU_restoreBookmarksFromJSONFile(aFile) {
  1060.     var failed = false;
  1061.     var obsServ = Cc["@mozilla.org/observer-service;1"].
  1062.                   getService(Ci.nsIObserverService);
  1063.     obsServ.notifyObservers(null,
  1064.                             RESTORE_BEGIN_NSIOBSERVER_TOPIC,
  1065.                             RESTORE_NSIOBSERVER_DATA);
  1066.  
  1067.     try {
  1068.       // open file stream
  1069.       var stream = Cc["@mozilla.org/network/file-input-stream;1"].
  1070.                    createInstance(Ci.nsIFileInputStream);
  1071.       stream.init(aFile, 0x01, 0, 0);
  1072.       var converted = Cc["@mozilla.org/intl/converter-input-stream;1"].
  1073.                       createInstance(Ci.nsIConverterInputStream);
  1074.       converted.init(stream, "UTF-8", 8192,
  1075.                      Ci.nsIConverterInputStream.DEFAULT_REPLACEMENT_CHARACTER);
  1076.  
  1077.       // read in contents
  1078.       var str = {};
  1079.       var jsonStr = "";
  1080.       while (converted.readString(8192, str) != 0)
  1081.         jsonStr += str.value;
  1082.       converted.close();
  1083.  
  1084.       if (jsonStr.length == 0)
  1085.         return; // empty file
  1086.  
  1087.       this.restoreBookmarksFromJSONString(jsonStr, true);
  1088.     }
  1089.     catch (exc) {
  1090.       failed = true;
  1091.       obsServ.notifyObservers(null,
  1092.                               RESTORE_FAILED_NSIOBSERVER_TOPIC,
  1093.                               RESTORE_NSIOBSERVER_DATA);
  1094.       Components.utils.reportError("Bookmarks JSON restore failed: " + exc);
  1095.       throw exc;
  1096.     }
  1097.     finally {
  1098.       if (!failed) {
  1099.         obsServ.notifyObservers(null,
  1100.                                 RESTORE_SUCCESS_NSIOBSERVER_TOPIC,
  1101.                                 RESTORE_NSIOBSERVER_DATA);
  1102.       }
  1103.     }
  1104.   },
  1105.  
  1106.   /**
  1107.    * Import bookmarks from a JSON string.
  1108.    * Note: any item annotated with "places/excludeFromBackup" won't be removed
  1109.    *       before executing the restore.
  1110.    * 
  1111.    * @param aString
  1112.    *        JSON string of serialized bookmark data.
  1113.    * @param aReplace
  1114.    *        Boolean if true, replace existing bookmarks, else merge.
  1115.    */
  1116.   restoreBookmarksFromJSONString:
  1117.   function PU_restoreBookmarksFromJSONString(aString, aReplace) {
  1118.     // convert string to JSON
  1119.     var nodes = this.unwrapNodes(aString, this.TYPE_X_MOZ_PLACE_CONTAINER);
  1120.  
  1121.     if (nodes.length == 0 || !nodes[0].children ||
  1122.         nodes[0].children.length == 0)
  1123.       return; // nothing to restore
  1124.  
  1125.     // ensure tag folder gets processed last
  1126.     nodes[0].children.sort(function sortRoots(aNode, bNode) {
  1127.       return (aNode.root && aNode.root == "tagsFolder") ? 1 :
  1128.               (bNode.root && bNode.root == "tagsFolder") ? -1 : 0;
  1129.     });
  1130.  
  1131.     var batch = {
  1132.       _utils: this,
  1133.       nodes: nodes[0].children,
  1134.       runBatched: function restore_runBatched() {
  1135.         if (aReplace) {
  1136.           // Get roots excluded from the backup, we will not remove them
  1137.           // before restoring.
  1138.           var excludeItems = this._utils.annotations
  1139.                                  .getItemsWithAnnotation(EXCLUDE_FROM_BACKUP_ANNO, {});
  1140.           // delete existing children of the root node, excepting:
  1141.           // 1. special folders: delete the child nodes
  1142.           // 2. tags folder: untag via the tagging api
  1143.           var query = this._utils.history.getNewQuery();
  1144.           query.setFolders([this._utils.placesRootId], 1);
  1145.           var options = this._utils.history.getNewQueryOptions();
  1146.           options.expandQueries = false;
  1147.           var root = this._utils.history.executeQuery(query, options).root;
  1148.           root.containerOpen = true;
  1149.           var childIds = [];
  1150.           for (var i = 0; i < root.childCount; i++) {
  1151.             var childId = root.getChild(i).itemId;
  1152.             if (excludeItems.indexOf(childId) == -1)
  1153.               childIds.push(childId);
  1154.           }
  1155.           root.containerOpen = false;
  1156.  
  1157.           for (var i = 0; i < childIds.length; i++) {
  1158.             var rootItemId = childIds[i];
  1159.             if (rootItemId == this._utils.tagsFolderId) {
  1160.               // remove tags via the tagging service
  1161.               var tags = this._utils.tagging.allTags;
  1162.               var uris = [];
  1163.               var bogusTagContainer = false;
  1164.               for (let i in tags) {
  1165.                 var tagURIs = [];
  1166.                 // skip empty tags since getURIsForTag would throw
  1167.                 if (tags[i])
  1168.                   tagURIs = this._utils.tagging.getURIsForTag(tags[i]);
  1169.  
  1170.                 if (!tagURIs.length) {
  1171.                   // This is a bogus tag container, empty tags should be removed
  1172.                   // automatically, but this does not work if they contain some
  1173.                   // not-uri node, so we remove them manually.
  1174.                   // XXX this is a temporary workaround until we implement
  1175.                   // preventive database maintenance in bug 431558.
  1176.                   bogusTagContainer = true;
  1177.                 }
  1178.                 for (let j in tagURIs)
  1179.                   this._utils.tagging.untagURI(tagURIs[j], [tags[i]]);
  1180.               }
  1181.               if (bogusTagContainer)
  1182.                 this._utils.bookmarks.removeFolderChildren(rootItemId);
  1183.             }
  1184.             else if ([this._utils.toolbarFolderId,
  1185.                       this._utils.unfiledBookmarksFolderId,
  1186.                       this._utils.bookmarksMenuFolderId].indexOf(rootItemId) != -1)
  1187.               this._utils.bookmarks.removeFolderChildren(rootItemId);
  1188.             else
  1189.               this._utils.bookmarks.removeItem(rootItemId);
  1190.           }
  1191.         }
  1192.  
  1193.         var searchIds = [];
  1194.         var folderIdMap = [];
  1195.  
  1196.         this.nodes.forEach(function(node) {
  1197.           if (!node.children || node.children.length == 0)
  1198.             return; // nothing to restore for this root
  1199.  
  1200.           if (node.root) {
  1201.             var container = this.placesRootId; // default to places root
  1202.             switch (node.root) {
  1203.               case "bookmarksMenuFolder":
  1204.                 container = this.bookmarksMenuFolderId;
  1205.                 break;
  1206.               case "tagsFolder":
  1207.                 container = this.tagsFolderId;
  1208.                 break;
  1209.               case "unfiledBookmarksFolder":
  1210.                 container = this.unfiledBookmarksFolderId;
  1211.                 break;
  1212.               case "toolbarFolder":
  1213.                 container = this.toolbarFolderId;
  1214.                 break;
  1215.             }
  1216.  
  1217.             // insert the data into the db
  1218.             node.children.forEach(function(child) {
  1219.               var index = child.index;
  1220.               var [folders, searches] = this.importJSONNode(child, container, index);
  1221.               for (var i = 0; i < folders.length; i++) {
  1222.                 if (folders[i])
  1223.                   folderIdMap[i] = folders[i];
  1224.               }
  1225.               searchIds = searchIds.concat(searches);
  1226.             }, this);
  1227.           }
  1228.           else
  1229.             this.importJSONNode(node, this.placesRootId, node.index);
  1230.  
  1231.         }, this._utils);
  1232.  
  1233.         // fixup imported place: uris that contain folders
  1234.         searchIds.forEach(function(aId) {
  1235.           var oldURI = this.bookmarks.getBookmarkURI(aId);
  1236.           var uri = this._fixupQuery(this.bookmarks.getBookmarkURI(aId),
  1237.                                      folderIdMap);
  1238.           if (!uri.equals(oldURI))
  1239.             this.bookmarks.changeBookmarkURI(aId, uri);
  1240.         }, this._utils);
  1241.       }
  1242.     };
  1243.  
  1244.     this.bookmarks.runInBatchMode(batch, null);
  1245.   },
  1246.  
  1247.   /**
  1248.    * Takes a JSON-serialized node and inserts it into the db.
  1249.    *
  1250.    * @param   aData
  1251.    *          The unwrapped data blob of dropped or pasted data.
  1252.    * @param   aContainer
  1253.    *          The container the data was dropped or pasted into
  1254.    * @param   aIndex
  1255.    *          The index within the container the item was dropped or pasted at
  1256.    * @returns an array containing of maps of old folder ids to new folder ids,
  1257.    *          and an array of saved search ids that need to be fixed up.
  1258.    *          eg: [[[oldFolder1, newFolder1]], [search1]]
  1259.    */
  1260.   importJSONNode: function PU_importJSONNode(aData, aContainer, aIndex) {
  1261.     var folderIdMap = [];
  1262.     var searchIds = [];
  1263.     var id = -1;
  1264.     switch (aData.type) {
  1265.       case this.TYPE_X_MOZ_PLACE_CONTAINER:
  1266.         if (aContainer == PlacesUtils.bookmarks.tagsFolder) {
  1267.           // node is a tag
  1268.           if (aData.children) {
  1269.             aData.children.forEach(function(aChild) {
  1270.               try {
  1271.                 this.tagging.tagURI(this._uri(aChild.uri), [aData.title]);
  1272.               } catch (ex) {
  1273.                 // invalid tag child, skip it
  1274.               }
  1275.             }, this);
  1276.             return [folderIdMap, searchIds];
  1277.           }
  1278.         }
  1279.         else if (aData.livemark && aData.annos) {
  1280.           // node is a livemark
  1281.           var feedURI = null;
  1282.           var siteURI = null;
  1283.           aData.annos = aData.annos.filter(function(aAnno) {
  1284.             switch (aAnno.name) {
  1285.               case LMANNO_FEEDURI:
  1286.                 feedURI = this._uri(aAnno.value);
  1287.                 return false;
  1288.               case LMANNO_SITEURI:
  1289.                 siteURI = this._uri(aAnno.value);
  1290.                 return false;
  1291.               case LMANNO_EXPIRATION:
  1292.               case LMANNO_LOADING:
  1293.               case LMANNO_LOADFAILED:
  1294.                 return false;
  1295.               default:
  1296.                 return true;
  1297.             }
  1298.           }, this);
  1299.  
  1300.           if (feedURI) {
  1301.             id = this.livemarks.createLivemarkFolderOnly(aContainer,
  1302.                                                          aData.title,
  1303.                                                          siteURI, feedURI,
  1304.                                                          aIndex);
  1305.           }
  1306.         }
  1307.         else {
  1308.           id = this.bookmarks.createFolder(aContainer, aData.title, aIndex);
  1309.           folderIdMap[aData.id] = id;
  1310.           // process children
  1311.           if (aData.children) {
  1312.             aData.children.forEach(function(aChild, aIndex) {
  1313.               var [folders, searches] = this.importJSONNode(aChild, id, aIndex);
  1314.               for (var i = 0; i < folders.length; i++) {
  1315.                 if (folders[i])
  1316.                   folderIdMap[i] = folders[i];
  1317.               }
  1318.               searchIds = searchIds.concat(searches);
  1319.             }, this);
  1320.           }
  1321.         }
  1322.         break;
  1323.       case this.TYPE_X_MOZ_PLACE:
  1324.         id = this.bookmarks.insertBookmark(aContainer, this._uri(aData.uri), aIndex, aData.title);
  1325.         if (aData.keyword)
  1326.           this.bookmarks.setKeywordForBookmark(id, aData.keyword);
  1327.         if (aData.tags) {
  1328.           var tags = aData.tags.split(", ");
  1329.           if (tags.length)
  1330.             this.tagging.tagURI(this._uri(aData.uri), tags);
  1331.         }
  1332.         if (aData.charset)
  1333.           this.history.setCharsetForURI(this._uri(aData.uri), aData.charset);
  1334.         if (aData.uri.substr(0, 6) == "place:")
  1335.           searchIds.push(id);
  1336.         break;
  1337.       case this.TYPE_X_MOZ_PLACE_SEPARATOR:
  1338.         id = this.bookmarks.insertSeparator(aContainer, aIndex);
  1339.         break;
  1340.       default:
  1341.         // Unknown node type
  1342.     }
  1343.  
  1344.     // set generic properties, valid for all nodes
  1345.     if (id != -1) {
  1346.       if (aData.dateAdded)
  1347.         this.bookmarks.setItemDateAdded(id, aData.dateAdded);
  1348.       if (aData.lastModified)
  1349.         this.bookmarks.setItemLastModified(id, aData.lastModified);
  1350.       if (aData.annos && aData.annos.length)
  1351.         this.setAnnotationsForItem(id, aData.annos);
  1352.     }
  1353.  
  1354.     return [folderIdMap, searchIds];
  1355.   },
  1356.  
  1357.   /**
  1358.    * Replaces imported folder ids with their local counterparts in a place: URI.
  1359.    *
  1360.    * @param   aURI
  1361.    *          A place: URI with folder ids.
  1362.    * @param   aFolderIdMap
  1363.    *          An array mapping old folder id to new folder ids.
  1364.    * @returns the fixed up URI if all matched. If some matched, it returns
  1365.    *          the URI with only the matching folders included. If none matched it
  1366.    *          returns the input URI unchanged.
  1367.    */
  1368.   _fixupQuery: function PU__fixupQuery(aQueryURI, aFolderIdMap) {
  1369.     function convert(str, p1, offset, s) {
  1370.       return "folder=" + aFolderIdMap[p1];
  1371.     }
  1372.     var stringURI = aQueryURI.spec.replace(/folder=([0-9]+)/g, convert);
  1373.     return this._uri(stringURI);
  1374.   },
  1375.  
  1376.   /**
  1377.    * Serializes the given node (and all its descendents) as JSON
  1378.    * and writes the serialization to the given output stream.
  1379.    * 
  1380.    * @param   aNode
  1381.    *          An nsINavHistoryResultNode
  1382.    * @param   aStream
  1383.    *          An nsIOutputStream. NOTE: it only uses the write(str, len)
  1384.    *          method of nsIOutputStream. The caller is responsible for
  1385.    *          closing the stream.
  1386.    * @param   aIsUICommand
  1387.    *          Boolean - If true, modifies serialization so that each node self-contained.
  1388.    *          For Example, tags are serialized inline with each bookmark.
  1389.    * @param   aResolveShortcuts
  1390.    *          Converts folder shortcuts into actual folders. 
  1391.    * @param   aExcludeItems
  1392.    *          An array of item ids that should not be written to the backup.
  1393.    */
  1394.   serializeNodeAsJSONToOutputStream:
  1395.   function PU_serializeNodeAsJSONToOutputStream(aNode, aStream, aIsUICommand,
  1396.                                                 aResolveShortcuts,
  1397.                                                 aExcludeItems) {
  1398.     var self = this;
  1399.     
  1400.     function addGenericProperties(aPlacesNode, aJSNode) {
  1401.       aJSNode.title = aPlacesNode.title;
  1402.       var id = aPlacesNode.itemId;
  1403.       if (id != -1) {
  1404.         aJSNode.id = id;
  1405.  
  1406.         var parent = aPlacesNode.parent;
  1407.         if (parent)
  1408.           aJSNode.parent = parent.itemId;
  1409.         var dateAdded = aPlacesNode.dateAdded;
  1410.         if (dateAdded)
  1411.           aJSNode.dateAdded = dateAdded;
  1412.         var lastModified = aPlacesNode.lastModified;
  1413.         if (lastModified)
  1414.           aJSNode.lastModified = lastModified;
  1415.  
  1416.         // XXX need a hasAnnos api
  1417.         var annos = [];
  1418.         try {
  1419.           annos = self.getAnnotationsForItem(id).filter(function(anno) {
  1420.             // XXX should whitelist this instead, w/ a pref for
  1421.             // backup/restore of non-whitelisted annos
  1422.             // XXX causes JSON encoding errors, so utf-8 encode
  1423.             //anno.value = unescape(encodeURIComponent(anno.value));
  1424.             if (anno.name == LMANNO_FEEDURI)
  1425.               aJSNode.livemark = 1;
  1426.             if (anno.name == READ_ONLY_ANNO && aResolveShortcuts) {
  1427.               // When copying a read-only node, remove the read-only annotation.
  1428.               return false;
  1429.             }
  1430.             return true;
  1431.           });
  1432.         } catch(ex) {
  1433.           LOG(ex);
  1434.         }
  1435.         if (annos.length != 0)
  1436.           aJSNode.annos = annos;
  1437.       }
  1438.       // XXXdietrich - store annos for non-bookmark items
  1439.     }
  1440.  
  1441.     function addURIProperties(aPlacesNode, aJSNode) {
  1442.       aJSNode.type = self.TYPE_X_MOZ_PLACE;
  1443.       aJSNode.uri = aPlacesNode.uri;
  1444.       if (aJSNode.id && aJSNode.id != -1) {
  1445.         // harvest bookmark-specific properties
  1446.         var keyword = self.bookmarks.getKeywordForBookmark(aJSNode.id);
  1447.         if (keyword)
  1448.           aJSNode.keyword = keyword;
  1449.       }
  1450.  
  1451.       var tags = aIsUICommand ? aPlacesNode.tags : null;
  1452.       if (tags)
  1453.         aJSNode.tags = tags;
  1454.  
  1455.       // last character-set
  1456.       var uri = self._uri(aPlacesNode.uri);
  1457.       var lastCharset = self.history.getCharsetForURI(uri);
  1458.       if (lastCharset)
  1459.         aJSNode.charset = lastCharset;
  1460.     }
  1461.  
  1462.     function addSeparatorProperties(aPlacesNode, aJSNode) {
  1463.       aJSNode.type = self.TYPE_X_MOZ_PLACE_SEPARATOR;
  1464.     }
  1465.  
  1466.     function addContainerProperties(aPlacesNode, aJSNode) {
  1467.       // saved queries
  1468.       var concreteId = PlacesUtils.getConcreteItemId(aPlacesNode);
  1469.       if (aJSNode.id != -1 && (PlacesUtils.nodeIsQuery(aPlacesNode) ||
  1470.           (concreteId != aPlacesNode.itemId && !aResolveShortcuts))) {
  1471.         aJSNode.type = self.TYPE_X_MOZ_PLACE;
  1472.         aJSNode.uri = aPlacesNode.uri;
  1473.         // folder shortcut
  1474.         if (aIsUICommand)
  1475.           aJSNode.concreteId = concreteId;
  1476.         return;
  1477.       }
  1478.       else if (aJSNode.id != -1) { // bookmark folder
  1479.         if (concreteId != aPlacesNode.itemId)
  1480.         aJSNode.type = self.TYPE_X_MOZ_PLACE;
  1481.         aJSNode.type = self.TYPE_X_MOZ_PLACE_CONTAINER;
  1482.         // mark special folders
  1483.         if (aJSNode.id == self.bookmarks.placesRoot)
  1484.           aJSNode.root = "placesRoot";
  1485.         else if (aJSNode.id == self.bookmarks.bookmarksMenuFolder)
  1486.           aJSNode.root = "bookmarksMenuFolder";
  1487.         else if (aJSNode.id == self.bookmarks.tagsFolder)
  1488.           aJSNode.root = "tagsFolder";
  1489.         else if (aJSNode.id == self.bookmarks.unfiledBookmarksFolder)
  1490.           aJSNode.root = "unfiledBookmarksFolder";
  1491.         else if (aJSNode.id == self.bookmarks.toolbarFolder)
  1492.           aJSNode.root = "toolbarFolder";
  1493.       }
  1494.     }
  1495.  
  1496.     function writeScalarNode(aStream, aNode) {
  1497.       // serialize to json
  1498.       var jstr = self.toJSONString(aNode);
  1499.       // write to stream
  1500.       aStream.write(jstr, jstr.length);
  1501.     }
  1502.  
  1503.     function writeComplexNode(aStream, aNode, aSourceNode) {
  1504.       var escJSONStringRegExp = /(["\\])/g;
  1505.       // write prefix
  1506.       var properties = [];
  1507.       for (let [name, value] in Iterator(aNode)) {
  1508.         if (name == "annos")
  1509.           value = self.toJSONString(value);
  1510.         else if (typeof value == "string")
  1511.           value = "\"" + value.replace(escJSONStringRegExp, '\\$1') + "\"";
  1512.         properties.push("\"" + name.replace(escJSONStringRegExp, '\\$1') + "\":" + value);
  1513.       }
  1514.       var jStr = "{" + properties.join(",") + ",\"children\":[";
  1515.       aStream.write(jStr, jStr.length);
  1516.  
  1517.       // write child nodes
  1518.       if (!aNode.livemark) {
  1519.         asContainer(aSourceNode);
  1520.         var wasOpen = aSourceNode.containerOpen;
  1521.         if (!wasOpen)
  1522.           aSourceNode.containerOpen = true;
  1523.         var cc = aSourceNode.childCount;
  1524.         for (var i = 0; i < cc; ++i) {
  1525.           var childNode = aSourceNode.getChild(i);
  1526.           if (aExcludeItems && aExcludeItems.indexOf(childNode.itemId) != -1)
  1527.             continue;
  1528.           var written = serializeNodeToJSONStream(aSourceNode.getChild(i), i);
  1529.           if (written && i < cc - 1)
  1530.             aStream.write(",", 1);
  1531.         }
  1532.         if (!wasOpen)
  1533.           aSourceNode.containerOpen = false;
  1534.       }
  1535.  
  1536.       // write suffix
  1537.       aStream.write("]}", 2);
  1538.     }
  1539.  
  1540.     function serializeNodeToJSONStream(bNode, aIndex) {
  1541.       var node = {};
  1542.  
  1543.       // set index in order received
  1544.       // XXX handy shortcut, but are there cases where we don't want
  1545.       // to export using the sorting provided by the query?
  1546.       if (aIndex)
  1547.         node.index = aIndex;
  1548.  
  1549.       addGenericProperties(bNode, node);
  1550.  
  1551.       var parent = bNode.parent;
  1552.       var grandParent = parent ? parent.parent : null;
  1553.  
  1554.       if (self.nodeIsURI(bNode)) {
  1555.         // Tag root accept only folder nodes
  1556.         if (parent && parent.itemId == self.tagsFolderId)
  1557.           return false;
  1558.         // Check for url validity, since we can't halt while writing a backup.
  1559.         // This will throw if we try to serialize an invalid url and it does
  1560.         // not make sense saving a wrong or corrupt uri node.
  1561.         try {
  1562.           self._uri(bNode.uri);
  1563.         } catch (ex) {
  1564.           return false;
  1565.         }
  1566.         addURIProperties(bNode, node);
  1567.       }
  1568.       else if (self.nodeIsContainer(bNode)) {
  1569.         // Tag containers accept only uri nodes
  1570.         if (grandParent && grandParent.itemId == self.tagsFolderId)
  1571.           return false;
  1572.         addContainerProperties(bNode, node);
  1573.       }
  1574.       else if (self.nodeIsSeparator(bNode)) {
  1575.         // Tag root accept only folder nodes
  1576.         // Tag containers accept only uri nodes
  1577.         if ((parent && parent.itemId == self.tagsFolderId) ||
  1578.             (grandParent && grandParent.itemId == self.tagsFolderId))
  1579.           return false;
  1580.  
  1581.         addSeparatorProperties(bNode, node);
  1582.       }
  1583.  
  1584.       if (!node.feedURI && node.type == self.TYPE_X_MOZ_PLACE_CONTAINER)
  1585.         writeComplexNode(aStream, node, bNode);
  1586.       else
  1587.         writeScalarNode(aStream, node);
  1588.       return true;
  1589.     }
  1590.  
  1591.     // serialize to stream
  1592.     serializeNodeToJSONStream(aNode, null);
  1593.   },
  1594.  
  1595.   /**
  1596.    * Serialize a JS object to JSON
  1597.    */
  1598.   toJSONString: function PU_toJSONString(aObj) {
  1599.     var JSON = Cc["@mozilla.org/dom/json;1"].createInstance(Ci.nsIJSON);
  1600.     return JSON.encode(aObj);
  1601.   },
  1602.  
  1603.   /**
  1604.    * backupBookmarksToFile()
  1605.    *
  1606.    * Serializes bookmarks using JSON, and writes to the supplied file.
  1607.    * Note: any item that should not be backed up must be annotated with
  1608.    *       "places/excludeFromBackup".
  1609.    *
  1610.    * @param aFile
  1611.    *        nsIFile where to save JSON backup.
  1612.    */
  1613.   backupBookmarksToFile: function PU_backupBookmarksToFile(aFile) {
  1614.     if (aFile.exists() && !aFile.isWritable())
  1615.       return; // XXX
  1616.  
  1617.     // init stream
  1618.     var stream = Cc["@mozilla.org/network/file-output-stream;1"].
  1619.                  createInstance(Ci.nsIFileOutputStream);
  1620.     stream.init(aFile, 0x02 | 0x08 | 0x20, 0600, 0);
  1621.  
  1622.     // utf-8 converter stream
  1623.     var converter = Cc["@mozilla.org/intl/converter-output-stream;1"].
  1624.                  createInstance(Ci.nsIConverterOutputStream);
  1625.     converter.init(stream, "UTF-8", 0, 0x0000);
  1626.  
  1627.     // weep over stream interface variance
  1628.     var streamProxy = {
  1629.       converter: converter,
  1630.       write: function(aData, aLen) {
  1631.         this.converter.writeString(aData);
  1632.       }
  1633.     };
  1634.  
  1635.     // Get itemIds to be exluded from the backup
  1636.     var excludeItems = this.annotations
  1637.                            .getItemsWithAnnotation(EXCLUDE_FROM_BACKUP_ANNO, {});
  1638.  
  1639.     // query places root
  1640.     var options = this.history.getNewQueryOptions();
  1641.     options.expandQueries = false;
  1642.     var query = this.history.getNewQuery();
  1643.     query.setFolders([this.bookmarks.placesRoot], 1);
  1644.     var result = this.history.executeQuery(query, options);
  1645.     result.root.containerOpen = true;
  1646.     // serialize as JSON, write to stream
  1647.     this.serializeNodeAsJSONToOutputStream(result.root, streamProxy,
  1648.                                            false, false, excludeItems);
  1649.     result.root.containerOpen = false;
  1650.  
  1651.     // close converter and stream
  1652.     converter.close();
  1653.     stream.close();
  1654.   },
  1655.  
  1656.   /**
  1657.    * Creates a filename for bookmarks backup files.
  1658.    *
  1659.    * @param [optional] aDateObj Date object used to build the filename.
  1660.    *                            Will use current date if empty.
  1661.    * @return A bookmarks backup filename.
  1662.    */
  1663.   getBackupFilename:
  1664.   function PU_getBackupFilename(aDateObj) {
  1665.     if (!aDateObj)
  1666.       aDateObj = new Date();
  1667.     // Use YYYY-MM-DD (ISO 8601) as it doesn't contain illegal characters
  1668.     // and makes the alphabetical order of multiple backup files more useful.
  1669.     var date = aDateObj.toLocaleFormat("%Y-%m-%d");
  1670.     return "bookmarks-" + date + ".json";
  1671.   },
  1672.  
  1673.   /**
  1674.    * ArchiveBookmarksFile()
  1675.    *
  1676.    * Creates a dated backup once a day in <profile>/bookmarkbackups.
  1677.    * Stores the bookmarks using JSON.
  1678.    * Note: any item that should not be backed up must be annotated with
  1679.    *       "places/excludeFromBackup".
  1680.    *
  1681.    * @param int aNumberOfBackups - the maximum number of backups to keep
  1682.    *
  1683.    * @param bool aForceArchive - forces creating an archive even if one was 
  1684.    *                             already created that day (overwrites)
  1685.    */
  1686.   archiveBookmarksFile:
  1687.   function PU_archiveBookmarksFile(aNumberOfBackups, aForceArchive) {
  1688.     // get/create backups directory
  1689.     var dirService = Cc["@mozilla.org/file/directory_service;1"].
  1690.                      getService(Ci.nsIProperties);
  1691.     var bookmarksBackupDir = dirService.get("ProfD", Ci.nsILocalFile);
  1692.     bookmarksBackupDir.append("bookmarkbackups");
  1693.     if (!bookmarksBackupDir.exists()) {
  1694.       bookmarksBackupDir.create(Ci.nsIFile.DIRECTORY_TYPE, 0700);
  1695.       if (!bookmarksBackupDir.exists())
  1696.         return; // unable to create directory!
  1697.     }
  1698.  
  1699.     // Construct the new leafname.
  1700.     var date = new Date();
  1701.     var backupFilename = this.getBackupFilename(date);
  1702.     var backupFile = null;
  1703.     if (!aForceArchive) {
  1704.       var backupFileNames = [];
  1705.       var backupFilenamePrefix = backupFilename.substr(0, backupFilename.indexOf("-"));
  1706.  
  1707.       // Get the localized backup filename, to clear out
  1708.       // old backups with a localized name (bug 445704).
  1709.       var localizedFilename = this.getFormattedString("bookmarksArchiveFilename", [date]);
  1710.       var localizedFilenamePrefix = localizedFilename.substr(0, localizedFilename.indexOf("-"));
  1711.       var rx = new RegExp("^(bookmarks|" + localizedFilenamePrefix + ")-([0-9-]+)\.(json|html)");
  1712.  
  1713.       var entries = bookmarksBackupDir.directoryEntries;
  1714.       while (entries.hasMoreElements()) {
  1715.         var entry = entries.getNext().QueryInterface(Ci.nsIFile);
  1716.         var backupName = entry.leafName;
  1717.         // A valid backup is any file that matches either the localized or
  1718.         // not-localized filename (bug 445704).
  1719.         var matches = backupName.match(rx);
  1720.         if (matches) {
  1721.           if (backupName == backupFilename)
  1722.             backupFile = entry;
  1723.           backupFileNames.push({ filename: backupName, date: matches[2] });
  1724.         }
  1725.       }
  1726.  
  1727.       var numberOfBackupsToDelete = 0;
  1728.       if (aNumberOfBackups > -1)
  1729.         numberOfBackupsToDelete = backupFileNames.length - aNumberOfBackups;
  1730.  
  1731.       if (numberOfBackupsToDelete > 0) {
  1732.         // If we don't have today's backup, remove one more so that
  1733.         // the total backups after this operation does not exceed the
  1734.         // number specified in the pref.
  1735.         if (!backupFile)
  1736.           numberOfBackupsToDelete++;
  1737.         backupFileNames.sort(function compare(a, b) {
  1738.           return a.date < b.date ? -1 : a.date > b.date ? 1 : 0;
  1739.         });
  1740.         while (numberOfBackupsToDelete--) {
  1741.           let backupFile = bookmarksBackupDir.clone();
  1742.           backupFile.append(backupFileNames[0].filename);
  1743.           backupFile.remove(false);
  1744.           backupFileNames.shift();
  1745.         }
  1746.       }
  1747.  
  1748.       // do nothing if we either have today's backup already
  1749.       // or the user has set the pref to zero.
  1750.       if (backupFile || aNumberOfBackups == 0)
  1751.         return;
  1752.     }
  1753.  
  1754.     backupFile = bookmarksBackupDir.clone();
  1755.     backupFile.append(backupFilename);
  1756.  
  1757.     if (aForceArchive && backupFile.exists())
  1758.         backupFile.remove(false);
  1759.  
  1760.     if (!backupFile.exists())
  1761.       backupFile.create(Ci.nsIFile.NORMAL_FILE_TYPE, 0600);
  1762.  
  1763.     this.backupBookmarksToFile(backupFile);
  1764.   },
  1765.  
  1766.   /**
  1767.    * Get the most recent backup file.
  1768.    * @returns nsIFile backup file
  1769.    */
  1770.   getMostRecentBackup: function PU_getMostRecentBackup() {
  1771.     var dirService = Cc["@mozilla.org/file/directory_service;1"].
  1772.                      getService(Ci.nsIProperties);
  1773.     var bookmarksBackupDir = dirService.get("ProfD", Ci.nsILocalFile);
  1774.     bookmarksBackupDir.append("bookmarkbackups");
  1775.     if (!bookmarksBackupDir.exists())
  1776.       return null;
  1777.  
  1778.     var backups = [];
  1779.     var entries = bookmarksBackupDir.directoryEntries;
  1780.     while (entries.hasMoreElements()) {
  1781.       var entry = entries.getNext().QueryInterface(Ci.nsIFile);
  1782.       if (!entry.isHidden() && entry.leafName.match(/^bookmarks-.+(html|json)?$/))
  1783.         backups.push(entry.leafName);
  1784.     }
  1785.  
  1786.     if (backups.length ==  0)
  1787.       return null;
  1788.  
  1789.     backups.sort();
  1790.     var filename = backups.pop();
  1791.  
  1792.     var backupFile = bookmarksBackupDir.clone();
  1793.     backupFile.append(filename);
  1794.     return backupFile;
  1795.   },
  1796.  
  1797.   /**
  1798.    * Starts the database coherence check and executes update tasks on a timer,
  1799.    * this method is called by browser.js in delayed startup.
  1800.    */
  1801.   startPlacesDBUtils: function PU_startPlacesDBUtils() {
  1802.     Components.utils.import("resource://gre/modules/PlacesDBUtils.jsm");
  1803.   }
  1804. };
  1805.